﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Xml;
using NUnit.Framework;
using PropertyExpression.ControllableQuery;
using PropertyExpression.ControllableQuery.Test.Queries;

namespace PropertyExpression.ControllableQueryTool.Test
{
    [TestFixture]
    public class MainTest
    {
        [Test]
        public void Test()
        {
            foreach (var type in GetTypes(typeof (PartLinkQuery)))
            {
                foreach (var @interface in type.GetInterfaces())
                {                    
                    if (InterfaceIsGenericAndEquals(@interface, typeof(IQuery<>)))
                    {
                        ProcessQueryType(type, @interface.GetGenericArguments().Single());
                    }
                }
            }
        }

        private static IEnumerable<Type> GetTypes(Type type)
        {
            //TODO: перебирать Type-ы во всех сборках
            return type.Assembly.GetTypes();
        }

        private class ParamInfo
        {
            public readonly string Name;
            public readonly FieldInfo FieldInfo;
            public readonly Func<ParamInfo, Tuple<string, string>> SqlDeclareInfoFunc;
            public readonly Func<ParamInfo, XmlDocument, XmlNamespaceManager, string> SqlTypeFunc;

            public ParamInfo(
                string name, 
                FieldInfo fieldInfo,
                Func<ParamInfo,
                Tuple<string, string>> sqlDeclareInfoFunc,
                Func<ParamInfo, XmlDocument, XmlNamespaceManager, string> sqlTypeFunc)
            {
                Name = name;
                FieldInfo = fieldInfo;
                SqlDeclareInfoFunc = sqlDeclareInfoFunc;
                SqlTypeFunc = sqlTypeFunc;
            }
        }

        private static void ProcessQueryType(Type queryType, Type dataInfoType)
        {
            Console.WriteLine(dataInfoType);
            Console.WriteLine("@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
            foreach (var queryCreator in InvokeConstructor(queryType))
            {
                var paramInfoList = new List<ParamInfo>();
                var queryText = ((IQuery) queryCreator(paramInfoList)).TransformText();

                var executionPlanXml = WithSqlCommand(
                    sqlCommand =>
                        {
                            sqlCommand.CommandText = "SET SHOWPLAN_XML ON";
                            sqlCommand.ExecuteNonQuery();

                            sqlCommand.CommandText = GetCommandText(
                                paramInfoList.Select(
                                    paramInfo => paramInfo.SqlDeclareInfoFunc(paramInfo)
                                    ),
                                queryText);
                            var result = sqlCommand.ExecuteScalar();

                            sqlCommand.CommandText = "SET SHOWPLAN_XML OFF";
                            sqlCommand.ExecuteNonQuery();

                            return result;
                        }
                    );

                var xmlDocument = new XmlDocument();
                xmlDocument.LoadXml((string) executionPlanXml);
                var xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable);
                xmlNamespaceManager.AddNamespace("a", "http://schemas.microsoft.com/sqlserver/2004/07/showplan");

                //TODO:
                Console.WriteLine(queryText);
                Console.WriteLine("#################");

                var sqlParamInfoList = paramInfoList.Select(
                    paramInfo =>
                    new
                        {
                            paramInfo,
                            SqlType = paramInfo.SqlTypeFunc(paramInfo, xmlDocument, xmlNamespaceManager)
                        }
                    );

                foreach (var sqlParamInfo in sqlParamInfoList)
                {
                    Console.WriteLine("{0} {1} {2}",
                                      sqlParamInfo.paramInfo.Name,
                                      sqlParamInfo.paramInfo.FieldInfo.FieldType,
                                      sqlParamInfo.SqlType);
                }

                Console.WriteLine("****************************");


                WithSqlCommand(
                    sqlCommand =>
                        {
                            sqlCommand.CommandText = GetCommandText(
                                sqlParamInfoList.Select(
                                    sqlParamInfo => Tuple.Create(sqlParamInfo.paramInfo.Name, sqlParamInfo.SqlType)
                                    ),
                                queryText);

                            DataTable schemaTable;
                            using (var sqlDataReader = sqlCommand.ExecuteReader(CommandBehavior.SchemaOnly))
                            {
                                schemaTable = sqlDataReader.GetSchemaTable();
                            }
                            foreach (DataRow dataRow in schemaTable.Rows)
                            {
                                Console.WriteLine(@"{0} {1} '{2}'",
                                                  dataRow["ColumnName"],
                                                  dataRow["DataType"],
                                                  dataRow["AllowDBNull"]);
                            }
                        }
                    );

                Console.WriteLine("------------------------------");

                foreach (var propertyInfo in dataInfoType.GetProperties())
                {
                    Console.WriteLine("{0} {1}", propertyInfo.Name, propertyInfo.PropertyType);
                }

                Console.WriteLine("****************************");
            }
        }

        private static string GetCommandText(IEnumerable<Tuple<string, string>> paramInfoList, string queryText)
        {
            var paramStringBuilder = new StringBuilder();
            foreach (var paramInfo in paramInfoList)
            {
                paramStringBuilder.AppendFormat("DECLARE {0} {1}", paramInfo.Item1, paramInfo.Item2);
                paramStringBuilder.AppendLine();
            }
            return string.Format(@"{0}
{1}",
                                 paramStringBuilder,
                                 queryText);
        }

        private static TResult WithSqlCommand<TResult>(Func<SqlCommand, TResult> func)
        {
            using (var connection = new SqlConnection(
                @"Data Source=(local)\MSSQLSERVER2008;Initial Catalog=ControllableQuery;Integrated Security=True"))
            {
                connection.Open();

                using (var sqlCommand = new SqlCommand())
                {
                    sqlCommand.Connection = connection;

                    return func(sqlCommand);
                }
            }
        }

        private static void WithSqlCommand(Action<SqlCommand> action)
        {
            WithSqlCommand(
                command =>
                    {
                        action(command);
                        return new {};
                    }
                );
        }

        private delegate object QueryCreator(List<ParamInfo> paramList);

        private static IEnumerable<IEnumerable<QueryCreator>> GetArgRepository<T>(
            IEnumerable<T> parameterInfos,
            Func<T, IEnumerable<QueryCreator>> choiceInstancesFunc)
        {
            IEnumerable<List<QueryCreator>> argRepository = new List<List<QueryCreator>>();
            foreach (var parameterInfo in parameterInfos)
            {
                var list = new List<List<List<QueryCreator>>>();
                foreach (var choiceInstance in choiceInstancesFunc(parameterInfo))
                {
                    if (argRepository.Count() == 0)
                    {
                        list.Add(new List<List<QueryCreator>> { new List<QueryCreator> { choiceInstance } });
                    }
                    else
                    {
                        var nextArgRepository = new List<List<QueryCreator>>();
                        foreach (var argList in argRepository)
                        {
                            nextArgRepository.Add(new List<QueryCreator>(argList) { choiceInstance });
                        }
                        list.Add(nextArgRepository);
                    }
                }
                argRepository = list.SelectMany(_ => _);
            }
            return argRepository;
        }

        private static IEnumerable<IEnumerable<QueryCreator>> GetArgRepository(IEnumerable<ParameterInfo> parameterInfos)
        {
            return GetArgRepository(parameterInfos, parameterInfo => GetChoiceInstances(parameterInfo.ParameterType));
        }

        private static Type MakeListType(Type itemType)
        {
            return typeof (List<>).MakeGenericType(itemType);
        }

        private static object GetList(Type listType)
        {
            return listType.GetConstructor(new Type[] {}).Invoke(new object[] {});
        }

        private static IEnumerable<QueryCreator> GetImplementationsForEnumerable(Type itemType, int count)
        {
            return GetArgRepository(
                Enumerable.Repeat(new {}, count),
                delegate
                    {
                        return InvokeConstructor(itemType);
                    }
                ).Select<IEnumerable<QueryCreator>, QueryCreator>(
                    queryCreatorEnumerable =>
                    paramList =>
                        {
                            var listType = MakeListType(itemType);
                            var methodInfo = listType.GetMethod("Add");
                            var list = GetList(listType);
                            foreach (var item in queryCreatorEnumerable.Select(queryCreator => queryCreator(paramList)))
                            {
                                methodInfo.Invoke(list, new[] {item});
                            }
                            return list;
                        }
                );
        }

        private static IEnumerable<QueryCreator> GetChoiceInstances(Type type)
        {
            if (InterfaceIsGenericAndEquals(type, typeof(IParams<>)))
            {
                return new QueryCreator[]
                           {
                               paramList => GetConstructorInfo(typeof (Params<>).MakeGenericType(
                                   type.GetGenericArguments().ToArray())
                                   ).Invoke(new object[] {paramList})
                           };
            }
            else if (typeof(bool).Equals(type))
            {
                return new QueryCreator[] { delegate { return false; }, delegate { return true; }};
            }
            else if (InterfaceIsGenericAndEquals(type, typeof(IEnumerable<>)))
            {
                var itemType = type.GetGenericArguments().Single();

                return new[]
                           {
                               new QueryCreator[] {delegate { return GetList(MakeListType(itemType)); }},
                               GetImplementationsForEnumerable(itemType, 1),
                               GetImplementationsForEnumerable(itemType, 2),
                               GetImplementationsForEnumerable(itemType, 3)
                           }.SelectMany(_ => _);
            }
            else if (type.IsInterface)
            {
                return GetImplementations(type).SelectMany(InvokeConstructor);
            }
            else
            {
                throw new ApplicationException();
            }
        }

        private class Params<T>: IParams<T>
        {
            private readonly List<ParamInfo> paramList;

            public Params(List<ParamInfo> paramList)
            {
                this.paramList = paramList;
            }

            public string Param<TProperty>(Expression<Func<T, TProperty>> propertyExpression)
            {
                return ScalarParam(propertyExpression, GetParamNameFunc());
            }

            public string Param<TProperty>(Expression<Func<T, TProperty>> propertyExpression, int index)
            {
                return ScalarParam(propertyExpression, GetParamNameFunc(index));
            }

            public string Param<TItem>(Expression<Func<T, IEnumerable<TItem>>> propertyExpression)
            {
                return TableParam(propertyExpression, GetParamNameFunc());
            }

            public string Param<TItem>(Expression<Func<T, IEnumerable<TItem>>> propertyExpression, int index)
            {
                return TableParam(propertyExpression, GetParamNameFunc(index));
            }

            private string ScalarParam<TProperty>(Expression<Func<T, TProperty>> propertyExpression, Func<FieldInfo, string> paramNameFunc)
            {
                return Param(
                    propertyExpression,
                    paramNameFunc,
                    paramInfo => Tuple.Create(paramInfo.Name, "VARBINARY(MAX)"),
                    (paramInfo, xmlDocument, xmlNamespaceManager) => xmlDocument.SelectSingleNode(
                        string.Format(@"//a:ColumnReference[@Column='{0}']/ancestor::a:Convert/@DataType",
                                      paramInfo.Name), xmlNamespaceManager).InnerText
                    );
            }

            private string TableParam<TItem>(Expression<Func<T, IEnumerable<TItem>>> propertyExpression, Func<FieldInfo, string> paramNameFunc)
            {
                //TODO: делать проверку, что SQL тип имеет одинаковый набор полей с .NET типом TItem.
                return Param(
                    propertyExpression,
                    paramNameFunc,
                    paramInfo => Tuple.Create(paramInfo.Name, typeof (TItem).Name),
                    delegate { return typeof (TItem).Name; }
                    );
            }

            private string Param<TProperty>(
                Expression<Func<T, TProperty>> propertyExpression,
                Func<FieldInfo, string> paramNameFunc,
                Func<ParamInfo, Tuple<string, string>> sqlDeclareInfoFunc, 
                Func<ParamInfo, XmlDocument, XmlNamespaceManager, string> sqlTypeFunc)
            {
                var fieldInfo = propertyExpression.GetFieldInfo();
                var paramName = paramNameFunc(fieldInfo);
                paramList.Add(
                    new ParamInfo(
                        paramName,
                        fieldInfo,
                        sqlDeclareInfoFunc,
                        sqlTypeFunc
                        )
                    );
                return paramName;
            }

            private static Func<FieldInfo, string> GetParamNameFunc()
            {
                return fieldInfo => string.Format("@{0}", fieldInfo.Name);
            }

            private static Func<FieldInfo, string> GetParamNameFunc(int index)
            {
                return fieldInfo => string.Format("@{0}{1}", fieldInfo.Name, index);
            }
        }

        private static IEnumerable<QueryCreator> InvokeConstructor(Type implementationType)
        {
            var constructorInfo = GetConstructorInfo(implementationType);
            var parameterInfos = constructorInfo.GetParameters();
            if (parameterInfos.Length == 0)
            {
                return new QueryCreator[] {delegate { return constructorInfo.Invoke(new object[] {}); }};
            }
            return GetArgRepository(parameterInfos)
                .Select<IEnumerable<QueryCreator>, QueryCreator>(
                queryCreatorEnumerable => paramList => constructorInfo.Invoke(
                    queryCreatorEnumerable.Select(queryCreator => queryCreator(paramList)).ToArray()
                    )
                );
        }

        private static IEnumerable<Type> GetImplementations(Type interfaceType)
        {
            foreach (var type in GetTypes(interfaceType))
            {
                foreach (var @interface in type.GetInterfaces())
                {
                    if (interfaceType.IsGenericType)
                    {
                        if (@interface.IsGenericType)
                        {
                            if (interfaceType.GetGenericTypeDefinition().Equals(@interface.GetGenericTypeDefinition()))
                            {
                                var typeArguments = type.GetGenericArguments();
                                var dictionary = typeArguments.Select((type1, index) => new {type = type1, index})
                                    .ToDictionary(arg => arg.type, arg => arg.index);
                                var interfaceArguments = @interface.GetGenericArguments();
                                var inputInterfaceArguments = interfaceType.GetGenericArguments();
                                for (var index = 0; index < interfaceArguments.Length; index++)
                                {
                                    typeArguments[dictionary[interfaceArguments[index]]] = inputInterfaceArguments[index];
                                }
                                yield return type.GetGenericTypeDefinition().MakeGenericType(typeArguments);
                            }
                        }
                    }
                    else
                    {
                        if (interfaceType.Equals(@interface))
                        {
                            yield return type;
                        }
                    }
                }
            }
        }

        private static ConstructorInfo GetConstructorInfo(Type type)
        {
            //TODO: написвать сообщение для эксепшена, если конструктор не один
            return type.GetConstructors().Single();
        }

        private static bool InterfaceIsGenericAndEquals(Type type1, Type typ2)
        {
            return type1.IsGenericType && typ2.Equals(type1.GetGenericTypeDefinition());
        }
    }
}